﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Direct.EntityStore;
using Direct.Security.Model;
using Direct.Security.Impl;
using Direct.Core;

namespace Direct.Security
{
    class AccountService : IAccountService
    {
        private IEntityStore _store;

        private class ObjectRelationDictionary : Dictionary<SecObjRelation, Dictionary<String, ISecurityObject>> { }

        private Dictionary<String, ISecurityObject> _objects = new Dictionary<string, ISecurityObject>();
        private Dictionary<String, ObjectRelationDictionary> _relations = new Dictionary<string, ObjectRelationDictionary>();

        public AccountService()
        {
            var _storeManager = ServiceLocator.GetInstance<IEntityStoreManager>();
            _store = _storeManager.CreateStore("accounts");
            _store.RegisterEntity<SecObjEntity>();
            _store.RegisterEntity<SecObjRelEntity>();
        }

        private SecObjEntity GetObjectEntity(string objId)
        {
            var qry = _store.CreateQuery<SecObjEntity>();
            qry.AddFilterEqual("ObjectId", objId);
            return qry.FindFirst();
        }

        private ISecurityObject StoreObject(SecObjEntity model)
        {
            var objType = ReflectionHelper.GetEnumItemByName<SecObjType>(model.ObjectType);
            ISecurityObject result = null;
            switch (objType)
            {
                case SecObjType.Default:
                    result = new DefaultPermissionsImpl(model);
                    break;
                case SecObjType.Role:
                    result = new RoleImpl(model);
                    break;
                case SecObjType.Schema:
                    result = new SchemaImpl(model);
                    break;
                case SecObjType.User:
                    var schema = GetSchemaInternal(model.ExtFld3, true);
                    result = new UserImpl(schema, model);
                    break;
            }
            Sync(() => _objects[result.ObjId] = result);
            return result;
        }

        private ISecurityObject GetSecurityObjectInternal(SecObjType objType, string objId, bool throwIfNotExists)
        {
            Assert.EmptyString(objId, "objId");
            objId = objId.ToLower();
            ISecurityObject result = null;
            if (!_objects.TryGetValue(objId, out result))
            {
                var model = GetObjectEntity(objId);
                if (model == null)
                {
                    if (throwIfNotExists)
                        throw new Exception("Security object of type {0}, id {1} not found.".Templ(objType, objId));
                    return null;
                }
                result = StoreObject(model);
            }

            if (result != null && result.ObjType != objType)
            {
                throw new Exception("The security object {0} is not of type {1}.".Templ(objId, objType));
            }
            return result;
        }

        private IUser GetUserInternal(String login, bool throwIfNotExists)
        {
            return GetSecurityObjectInternal(SecObjType.User, login, throwIfNotExists) as IUser;
        }

        private ISchema GetSchemaInternal(String schemaId, bool throwIfNotExists)
        {
            return GetSecurityObjectInternal(SecObjType.Schema, schemaId, throwIfNotExists) as ISchema;
        }

        public IRole GetRoleInternal(String roleName, bool throwIfNotExists)
        {
            return GetSecurityObjectInternal(SecObjType.Role, roleName, throwIfNotExists) as IRole;
        }

        public ISecurityObject GetObject(SecObjType objType, String objId)
        {
            return GetSecurityObjectInternal(objType, objId, false);
        }

        public ISecurityObject GetObject(String objId, bool throwIfNotExists = false)
        {
            ISecurityObject result = null;
            if (!_objects.TryGetValue(objId, out result))
            {
                var model = GetObjectEntity(objId);
                if (model != null)
                    return StoreObject(model);
            }
            if (result == null && throwIfNotExists)
                throw new Exception("Security object not found: {0}".Templ(objId));

            return result;
        }

        ISecurityObject IAccountService.GetObject(String objId)
        {
            return GetObject(objId, false);
        }

        private Object __lockObject = new Object();
        private void Sync(Action action)
        {
            lock (__lockObject)
            {
                action.Invoke();
            }
        }

        public IUser GetUser(string login)
        {
            return GetUserInternal(login, false);
        }

        public IRole GetRole(string roleName)
        {
            return GetRoleInternal(roleName, false);
        }

        public ISchema GetSchema(string schemaId)
        {
            return GetSchemaInternal(schemaId, false);
        }

        private bool[] ObjectLoadAllState = new bool[] { false, false, false, false };
        private T[] GetAllObjects<T>(SecObjType objType) where T : ISecurityObject
        {
            var qry = _store.CreateQuery<SecObjEntity>();
            qry.AddFilterEqual("ObjectType", objType);
            var objects = qry.Find();
            ISecurityObject obj = null;
            var result = new List<T>();
            foreach (var objEntity in objects)
            {
                if (!_objects.TryGetValue(objEntity.ObjectId, out obj))
                    obj = StoreObject(objEntity);
                result.Add((T)obj);
            }
            return result.ToArray();
        }


        public IUser[] GetUsers()
        {
            return GetAllObjects<IUser>(SecObjType.User);
        }

        public IRole[] GetRoles()
        {
            return GetAllObjects<IRole>(SecObjType.Role);
        }

        public ISchema[] GetSchemas()
        {
            return GetAllObjects<ISchema>(SecObjType.Schema);
        }

        public IUser[] GetUsersInSchema(string schemaId)
        {
            return GetUsers().Where(u => u.Schema.ObjId == schemaId).ToArray();
        }

        public IUser[] GetUsersInRole(string roleName)
        {
            return GetObjectRelations<IUser>(SecObjType.Role, roleName, SecObjRelation.UserToRole, true);
        }

        private T[] GetObjectRelations<T>(SecObjType secObjType, string objId, SecObjRelation secObjRelation, bool isTarget) where T : ISecurityObject
        {
            ObjectRelationDictionary objSlot = null;
            Dictionary<String, ISecurityObject> resDic;
            if (!_relations.TryGetValue(objId, out objSlot))
                _relations[objId] = objSlot = new ObjectRelationDictionary();

            if (!objSlot.TryGetValue(secObjRelation, out resDic))
            {
                objSlot[secObjRelation] = resDic = new Dictionary<string, ISecurityObject>();

                //procurar as relações
                var qry = _store.CreateQuery<SecObjRelEntity>();
                qry.AddFilterEqual("Rel", secObjRelation.ToString());
                qry.AddFilterEqual(isTarget ? "TgObjId" : "ObjId", objId);
                var models = qry.Find();
                foreach (var model in models)
                    resDic[model.ObjId] = GetObject(isTarget ? model.ObjId : model.TgObjId);
            }
            return resDic.Count == 0 ? new T[0] : resDic.Values.OrderBy(v => v.Name).Cast<T>().ToArray();
        }

        public IRole[] GetRolesForUser(string login)
        {
            return GetObjectRelations<IRole>(SecObjType.User, login, SecObjRelation.UserToRole, false);
        }

        public IRole[] GetRolesInSchema(string schemaId)
        {
            return GetObjectRelations<IRole>(SecObjType.Schema, schemaId, SecObjRelation.SchemaToRole, false);
        }

        public ISchema CreateSchema(string schemaId, string name, string description)
        {
            Assert.EmptyString(schemaId, "schemaId");
            Assert.EmptyString(name, "name");
            return CreateObject<ISchema>(SecObjType.Schema, schemaId, name, description, String.Empty, String.Empty, String.Empty);
        }

        public IRole CreateRole(string roleName, string group, string description)
        {
            Assert.EmptyString(roleName, "roleName");
            return CreateObject<IRole>(SecObjType.Role, roleName, roleName, description, group, String.Empty, String.Empty);
        }

        public IUser CreateUser(ISchema schema, string login, string name, string email, string description)
        {
            Assert.NullArgument(schema, "schema");
            Assert.EmptyString(login, "login");
            Assert.EmptyString(name, "name");
            Assert.EmptyString(email, "email");
            return CreateObject<IUser>(SecObjType.User, login, name, description, email, String.Empty, schema.ObjId);
        }

        private T CreateObject<T>(SecObjType secObjType, string objId, string objName, string description, string extFld1, string extFld2, String extFld3) where T : ISecurityObject
        {
            T result = default(T);
            Sync(() =>
            {
                //antes verificar se já existe um objeto com este id
                if (this.GetObject(objId) != null)
                    throw new InvalidOperationException("An object with id {0} already exists in accounts database.".Templ(objId));

                var entity = new SecObjEntity()
                {
                    ObjectType = secObjType.ToString(),
                    ObjectId = objId,
                    Name = objName,
                    Description = description,
                    ExtFld1 = extFld1,
                    ExtFld2 = extFld2,
                    ExtFld3 = extFld3,
                    Active = true
                };

                if (_store.Save(entity))
                    result = (T)StoreObject(entity);
                else
                    throw new Exception("Error on persist account object {0}".Templ(objId));
            });
            return result;
        }

        public void AddUserInRole(string login, string roleName)
        {
            var user = GetUserInternal(login, true);
            var role = GetRoleInternal(roleName, true);

            if (!role.IsInSchema(user.Schema))
                throw new Exception(String.Format("O papel {0} não faz parte do esquema ao qual o usuário {1} pertence.", roleName, login));
            AddObjectRelation(user.ObjId, SecObjRelation.UserToRole, role.ObjId);
        }

        private string RelationCacheKey(string sourceObjId, SecObjRelation secObjRelation, string targetObjId)
        {
            return String.Format("{0}({1},{2})".Templ(secObjRelation, sourceObjId, targetObjId)).ToLower();
        }

        private void AddObjectRelation(string sourceObjId, SecObjRelation secObjRelation, string targetObjId)
        {
            var source = GetObject(sourceObjId, true);
            var target = GetObject(targetObjId, true);

            SecObjType sourceType = secObjRelation == SecObjRelation.UserToRole ? SecObjType.User : SecObjType.Schema;
            SecObjType targetType = SecObjType.Role;

            if (target.ObjType != targetType || source.ObjType != sourceType)
                throw new InvalidOperationException("A security relation of type {0} must be between {1} object and a {2}. The relation between {3} and {4} is not possible.".Templ(secObjRelation, sourceType, targetType, source.ObjId, target.ObjId));

            var entity = GetRelationEntity(sourceObjId, secObjRelation, targetObjId);
            if (entity != null)
                return;

            //entidade
            entity = new SecObjRelEntity()
            {
                ObjId = sourceObjId.ToLower(),
                Rel = secObjRelation.ToString(),
                TgObjId = targetObjId.ToLower()
            };
            if (!_store.Save(entity))
                throw new Exception("Error on save security relation {0}({1},{2})".Templ(secObjRelation, sourceObjId, targetObjId));
            ResetRelationCache(secObjRelation, sourceObjId);
            ResetRelationCache(secObjRelation, targetObjId);
        }

        private void ResetRelationCache(SecObjRelation secObjRelation, string objId)
        {
            ObjectRelationDictionary objSlot = null;
            if (!_relations.TryGetValue(objId, out objSlot))
                return;
            objSlot.Remove(secObjRelation);
        }

        private SecObjRelEntity GetRelationEntity(string sourceObjId, SecObjRelation secObjRelation, string targetId)
        {
            var qry = _store.CreateQuery<SecObjRelEntity>();
            qry.AddFilterEqual("ObjId", sourceObjId)
               .AddFilterEqual("Rel", secObjRelation.ToString())
               .AddFilterEqual("TgObjId", targetId);
            return qry.FindFirst();
        }

        private void RemoveObjectRelation(string sourceObjId, SecObjRelation secObjRelation, string targetId)
        {
            var entity = GetRelationEntity(sourceObjId, secObjRelation, targetId);
            if (entity != null && _store.Remove(entity))
            {
                RemoveRelationCacheItem(secObjRelation, sourceObjId, targetId);
                RemoveRelationCacheItem(secObjRelation, targetId, sourceObjId);
            }
        }

        private void RemoveRelationCacheItem(SecObjRelation secObjRelation, string ownerObjId, string itemObjId)
        {
            ObjectRelationDictionary objSlot = null;
            if (!_relations.TryGetValue(ownerObjId, out objSlot))
                return;
            Dictionary<String, ISecurityObject> items;
            if (!objSlot.TryGetValue(secObjRelation, out items))
                return;
            items.Remove(itemObjId);
        }

        public void RemoveUserFromRole(string login, string roleName)
        {
            RemoveObjectRelation(login, SecObjRelation.UserToRole, roleName);
        }

        public void AddRoleInSchema(string roleName, string schemaId)
        {
            AddObjectRelation(schemaId, SecObjRelation.SchemaToRole, roleName);
        }

        public void RemoveRoleFromSchema(string roleName, string schemaId)
        {
            RemoveObjectRelation(schemaId, SecObjRelation.SchemaToRole, roleName);
        }

        public bool IsUserInRole(string login, string roleName)
        {
            Assert.EmptyString(login, "login");
            Assert.EmptyString(roleName, "roleName");
            return GetRolesForUser(login).Any(r => r.Name.EqualsIgnoreCase(roleName));
        }

        public bool IsRoleInSchema(string roleName, string schemaId)
        {
            Assert.EmptyString(roleName, "roleName");
            Assert.EmptyString(schemaId, "schemaId");
            return GetRolesInSchema(schemaId).Any(r => r.Name.EqualsIgnoreCase(roleName));
        }

        public void SetUserPassword(IUser user, string password)
        {
            var usr = user as UserImpl;
            var entity = GetObjectEntity(user.ObjId);
            if (password.IsEmpty())
                entity.ExtFld2 = "";
            else
                entity.ExtFld2 = new Token(new { objId = user.ObjId, pwd = password }).ToString();
            if (_store.Save(entity))
                StoreObject(entity);
        }
    }
}
